Capstone - Exchange Rate Analysis

Author

Brandon Dahl

Import Exchange Rates

All exchange rates were obtained from FRED, the Federal Reserve of St. Louis Economic Data database.

Exchange rates were downloaded for the following currencies:

  • Japan Yen to USD

  • USD to Euro

  • US to British Pound

  • USD to Australian Dollar

  • Canadian Dollar to USD

  • Chinese Yuan to UDS

All data will be converted as needed to Foreign Currency to USD for consistency in data analysis.

Source: Federal Reserve Bank of St. Louis

Code
# Clean Japan to US Exchange Data
exchange.jpy_to_usd_clean <- exchange.jpy_to_usd |> 
  rename(date = observation_date,
         JPY = DEXJPUS) |>
  # Fix Dates & Interpolate Missing Data
  mutate(date = as.Date(date),
         JPY = zoo::na.approx(JPY)) |> 
  # Filter To Last 25 Years
  filter(date >= start_date) |> 
  arrange(date)

# Clean Euro to US Exchange Data
exchange.eur_to_usd_clean <- exchange.eur_to_usd |> 
  rename(date = observation_date,
         EUR = DEXUSEU) |>
  # Fix Dates & Interpolate Missing Data
  mutate(date = as.Date(date),
         EUR = zoo::na.approx(EUR),
         EUR = 1/EUR) |> 
  # Filter To Last 25 Years
  filter(date >= start_date) |> 
  arrange(date)

# Clean GBP to US Exchange Data
exchange.gbp_to_usd_clean <- exchange.gbp_to_usd |> 
  rename(date = observation_date,
         GBP = DEXUSUK) |>
  # Fix Dates & Interpolate Missing
  mutate(date = as.Date(date),
         GBP = zoo::na.approx(GBP),
         GBP = 1/GBP) |> 
  # Filter To Last 25 Years
  filter(date >= start_date) |> 
  arrange(date)
  
# Clean AUD to US Exchange Data
exchange.aud_to_usd_clean <- exchange.aud_to_usd |> 
  rename(date = observation_date,
         AUD = DEXUSAL) |>
  # Fix Dates & Interpolate Missing
  mutate(date = as.Date(date),
         AUD = zoo::na.approx(AUD),
         AUD = 1/AUD) |> 
  # Filter To Last 25 Years
  filter(date >= start_date) |> 
   arrange(date)
   
# Clean CAD to US Exchange Data
exchange.cad_to_usd_clean <- exchange.cad_to_usd |> 
  rename(date = observation_date,
         CAD = DEXCAUS) |>
  # Fix Dates & Interpolate Missing
  mutate(date = as.Date(date),
         CAD = zoo::na.approx(CAD)) |> 
  # Filter To Last 25 Years
  filter(date >= start_date) |> 
  arrange(date)
   
# Clean CNY to US Exchange Data
exchange.cny_to_usd_clean <- exchange.cny_to_usd |> 
  rename(date = observation_date,
         CNY = DEXCHUS) |>
  # Fix Dates & Interpolate Missing
  mutate(date = as.Date(date),
         CNY = zoo::na.approx(CNY)) |> 
  # Filter To Last 25 Years
  filter(date >= start_date) |> 
   arrange(date)
   
# Create Joint Exchane Data
exchange_df <- exchange.jpy_to_usd_clean |> 
  left_join(x = _, y = exchange.eur_to_usd_clean, by = 'date') |> 
  left_join(x = _, y = exchange.aud_to_usd_clean, by = 'date') |>
  left_join(x = _, y = exchange.gbp_to_usd_clean, by = 'date') |>
  left_join(x = _, y = exchange.cad_to_usd_clean, by = 'date') |>
  left_join(x = _, y = exchange.cny_to_usd_clean, by = 'date') |> 
  arrange(date)

# Clear Original Data
rm(exchange.aud_to_usd, exchange.cad_to_usd, exchange.eur_to_usd, 
   exchange.jpy_to_usd, exchange.gbp_to_usd, exchange.cny_to_usd)

Exchange Rate Plots

Below is 25-years of historical exchange rates. The Japan/China exchange rates have been scaled to fit into one plot with the western currencies.

You can also see in the plot that the Chinese Yuan was pegged to the U.S. Dollar until roughly 2005.

Analyze Exchange Rates

Our analysis of the exchange rates will seek to find

  • Historical Daily Log Returns

  • Historical Daily Volatility

  • Historical Annualized Volatility

  • Correlation Between Currencies Daily Returns

Measure Volatility

Code
# Calculate Daily Log Returns
daily_returns <- exchange_df |>
    pivot_longer(cols = !starts_with('date'), names_to = 'currency', values_to = 'rate') |>
    arrange(currency, date) |>
    group_by(currency) |>
    mutate(daily_return = log(rate) - log(lag(rate, n = 1))) |> 
    ungroup() |>
    drop_na(daily_return)
  
# Calculate Daily Volatility
daily_volatility <- daily_returns |> 
    group_by(currency) |> 
    summarise(daily_volatility = sd(daily_return, na.rm = TRUE)) |> 
    ungroup()
    
# Convert To Annualize Volatility
annual_volatility <- daily_volatility |> 
  mutate(annual_volatility = daily_volatility*sqrt(252))
Currency to USD Volatility
Currency Daily Volatility Annualized Volatility
AUD 0.75% 11.94%
CAD 0.52% 8.30%
CNY 0.17% 2.74%
EUR 0.56% 8.90%
GBP 0.57% 9.07%
JPY 0.60% 9.54%

Correlation

The correlation matrix constructed in this analysis will be used to compute the correlated price patterns later using Geometric Brownian Motion.

As we can see from the correlation matrix and plots, the Western currencies (AUD, CAD, EUR, GBP) tend to be highly correlated with each other in their movements against the U.S. Dollar, while the Asian currencies exhibit very little correlation.

Code
# Create Correlation Matrix
daily_return_matrix <- daily_returns |>
  select(date, currency, daily_return) |> 
  pivot_wider(names_from = currency, values_from = daily_return) |> 
  select(-date) |> 
  cor()
Correlation Matrix
AUD CAD CNY EUR GBP JPY
AUD 100.00% 65.12% 22.89% 57.28% 55.42% 11.61%
CAD 65.12% 100.00% 17.50% 47.97% 47.05% 5.84%
CNY 22.89% 17.50% 100.00% 21.39% 23.22% 12.91%
EUR 57.28% 47.97% 21.39% 100.00% 64.86% 32.18%
GBP 55.42% 47.05% 23.22% 64.86% 100.00% 18.92%
JPY 11.61% 5.84% 12.91% 32.18% 18.92% 100.00%

Daily Return Distribution

Theoretically returns should be from a normal distribution to use Geometric Brownian Motion. We can see below that the returns are not normal, but for a simplistic model we’ll use GBM as has been done in several studies modeling exchange rates.

Historical Paths

Below we’ll examine 25-years of historical price paths to serve as a baseline indicator of how volatile exchange rates have been historically.

This will serve as a useful benchmark when we forecast possible exchange rate paths using GBM.

Code
# Calculate Historical Paths Wide
hist_paths <- exchange_df |> 
  mutate(YEAR = lubridate::year(date)) |> 
  group_by(YEAR) |> 
  arrange(date) |> 
  mutate(INDEX = row_number()) |> 
  ungroup() |> 
  arrange(YEAR, INDEX) |> 
  pivot_longer(cols = c('JPY','EUR','AUD','GBP','CAD','CNY'), names_to = 'COUNTRY', values_to = 'RATE') |> 
  select(-c(date)) |> 
  pivot_wider(names_from = YEAR, values_from = RATE) |> 
  arrange(COUNTRY, INDEX)

# Create Country Paths Long
hist_paths_long <- exchange_df |> 
  mutate(YEAR = lubridate::year(date)) |> 
  group_by(YEAR) |> 
  arrange(date) |> 
  mutate(INDEX = row_number()) |> 
  ungroup() |> 
  arrange(YEAR, INDEX) |> 
  pivot_longer(cols = c('JPY','EUR','AUD','GBP','CAD','CNY'), names_to = 'COUNTRY', values_to = 'RATE') |> 
  select(-c(date)) |> 
  arrange(COUNTRY, YEAR, INDEX)

# Create Adjusted Country Paths
hist_paths_adj <- hist_paths_long |> 
  group_by(YEAR, COUNTRY) |> 
  mutate(STARTING_RATE = first(RATE)) |> 
  ungroup() |> 
  mutate(RATE = RATE - STARTING_RATE)

# Create Adjusted Percentage Paths
hist_paths_pct <- hist_paths_long |> 
  group_by(YEAR, COUNTRY) |> 
  mutate(STARTING_RATE = first(RATE)) |> 
  ungroup() |> 
  mutate(RATE = (RATE - STARTING_RATE)/STARTING_RATE)

# Calculate Summary States
hist_path_stats <- hist_paths_pct |> 
  group_by(YEAR, COUNTRY) |> 
  filter(INDEX == max(INDEX)) |> 
  ungroup() |> 
  # Calculate Rankings
  group_by(COUNTRY) |>
  arrange(RATE) |> 
  mutate(RANK = row_number()) |> 
  ungroup() |> 
  mutate(BOTTOM_25 = if_else(RANK <= 6, RATE, NA_real_),
         TOP_25 = if_else(RANK >= 19, RATE, NA_real_ )) |> 
  arrange(COUNTRY) |> 
  group_by(COUNTRY) |> 
  summarise(MIN = min(RATE, na.rm = TRUE),
            MAX = max(RATE, na.rm = TRUE),
            AVG = mean(RATE, na.rm = TRUE),
            MIN_25TH = mean(BOTTOM_25, na.rm = TRUE),
            TOP_25TH = mean(TOP_25, na.rm = TRUE)) |> 
  ungroup()

Plots

Table

Foreign Exchange Historical Paths Statistics
Country Min Max Avg Min 25% Top 25%
AUD −25.17% 25.93% 0.29% −14.23% 12.51%
CAD −18.08% 23.62% 0.11% −10.95% 11.10%
CNY −6.53% 8.53% −0.40% −5.59% 4.33%
EUR −17.26% 13.80% −0.56% −12.19% 9.03%
GBP −11.69% 35.67% 0.70% −9.58% 12.38%
JPY −17.99% 21.16% 1.66% −10.96% 14.30%

Simulate Exchange Rate Paths

Below we’ll simulate the possible exchange rate paths for all six currencies using correlated Geometric Brownian Motion.

The formula for Geometric Brownian Motion is shown below:’

\(S_t = S_0 * exp((\mu - \frac12\sigma^2)t+\sigma W_t)\)

term

\(S_t\) = Asset Price at Time \(t\)

\(S_0\) = Initial Price of Asset

\(\mu\) = Drift Term (growth rate of asset path)

\(\sigma\) = Volatility

\(W_t\) = Standard Brownian Motion, or random variable from standard normal distribution.

Source: Correlated GBM Functions

Assumptions

For each country we’ll use their historical daily volatility as \(\sigma\), and we’ll assume a zero drift rate for all currencies.

Financial theory dictates that the differences in each countries respective risk free rate vs. the domestic currency may be used as the drift rate to indicate the cost of holding the one currency compared to the other. For this analysis we won’t be incorporating the risk free rates of return.

GBM Functions

Code
GBM <- function(N, sigma, mu, S0, Wt = NULL) {
  # Creates a single asset path of daily prices using Geometric Brownian Motion. 
  # One year is 252 days since that is about how many trading days are in any
  # given year.
  #
  # Args:
  #   N: Number of days in the path.
  #   sigma: Volatility or standard deviation of daily continuously compounded 
  #          returns.
  #   mu: Drift or average daily continuously compounded returns. 
  #   S0: The initial price of the asset. 
  #   Wt: The cumulative Brownian motion of the model. This can be supplied or 
  #       left as NULL. In the case that it is NULL, a vector will be provided.
  #       If you include this argument, it must be a vector of length N of the 
  #       cumulative sum of a random variable to work properly. 
  #
  # Returns:
  #   A vector of length N containing the asset prices generated by the specified
  #   GBM. 
  if (is.null(Wt)) {
    Wt <- cumsum(rnorm(N, 0, 1))
  }
  t <- (1:N)/252
  p1 <- (mu - 0.5*(sigma*sigma)) * t
  p2 <- sigma * Wt
  St = S0 * exp(p1 + p2)
  return(St)
}

CorrelatedGBM <- function(N, S0, mu, sigma, cor.mat) {
  # Creates a matrix of correlated daily price paths using Geometric 
  # Brownian Motion. 
  #
  # Args: 
  #   N: Number of days in the path.
  #   mu: Drift or average daily continuously compounded returns.  
  #   sigma: Volatility or standard deviation of daily continuously compounded 
  #          returns. 
  #   S0: The initial price of the asset. 
  #   cor.mat: The correlation matrix of the daility contiuously compounded 
  #            returns. 
  #
  # Returns:
  #   A matrix of simulated daily price paths of length N having the same number
  #   of assets as in the mu and sigma vectors. Note that mu and sigma must have
  #   the same dimensions. 
  mu <- as.matrix(mu)
  sigma <- as.matrix(sigma)
  GBMs <- matrix(nrow = N, ncol = nrow(mu))
  Wt <- matrix(rnorm(N * nrow(mu), 0, 1), ncol = nrow(mu))
  Wt <- apply(Wt, 2, cumsum)
  chol.mat <- chol(cor.mat) # upper triangular cholesky decomposition
  Wt <- Wt %*% chol.mat   # key trick for creating correlated paths
  for (i in 1:nrow(mu)) {
    GBMs[,i] <- GBM(N, sigma[i], mu[i] , S0[i], Wt[, i])
  }
  return(GBMs)
}

Compute Simulated GBM Paths

Code
# Parameters
set.seed (1)
hundreds <- 5
sims <- 100*hundreds

# Create Sim Path Aggregate
sim_path_agg <- data.frame()

# Create Simulated Exchange Rate Paths
for (s in 1:sims){
  
  # Create Correlated Path
  temp_corr_gbm <- CorrelatedGBM(N = 252,
                                 S0 = joint_data$current_rate,
                                 mu = rep(0,6),
                                 sigma = joint_data$daily_volatility,
                                 cor.mat = daily_return_matrix)
  
  # Clean Path Data
  temp_corr_gbm_df <- temp_corr_gbm |> 
    as.data.frame() |> 
    rename(!!joint_data$currency[1] := V1,
           !!joint_data$currency[2] := V2,
           !!joint_data$currency[3] := V3,
           !!joint_data$currency[4] := V4,
           !!joint_data$currency[5] := V5,
           !!joint_data$currency[6] := V6) |> 
    mutate(sim = s,
           days = row_number()) |> 
    relocate(c(days,sim), .before = everything()) |> 
    arrange(days)
  
  # Add To Aggregate
  sim_path_agg <- rbind(sim_path_agg,
                        temp_corr_gbm_df)
  
  # Kill Variables
  rm(temp_corr_gbm_df,temp_corr_gbm)

}

Compute Simulated Correlation

We’ll want to confirm the correlation of daily log returns of the simulated paths are roughly similar to historical return correlation.

Code
# Compute Returns
sim_path_agg_returns <- sim_path_agg |> 
  group_by(sim) |> 
  mutate(AUD_RETURNS = log(AUD) - log(lag(AUD, n = 1)),
         CAD_RETURNS = log(CAD) - log(lag(CAD, n = 1)),
         CNY_RETURNS = log(CNY) - log(lag(CNY, n = 1)),
         EUR_RETURNS = log(EUR) - log(lag(EUR, n = 1)),
         GBP_RETURNS = log(GBP) - log(lag(GBP, n = 1)),
         JPY_RETURNS = log(JPY) - log(lag(JPY, n = 1))) |> 
  ungroup() |> 
  drop_na() |> 
  select(days, sim, contains('RETURNS')) |> 
  arrange(sim, days)

# Calculate Correlation
sim_path_agg_returns_cor <- sim_path_agg_returns |>
  select(contains('RETURNS')) |> 
  cor()

# Reset Rownames
rownames(sim_path_agg_returns_cor) <- joint_data$currency
Simulated Exchange Path Correlations
AUD CAD CNY EUR GBP JPY
AUD 100.00% 65.00% 23.07% 57.40% 55.39% 11.79%
CAD 65.00% 100.00% 17.47% 48.04% 46.85% 5.78%
CNY 23.07% 17.47% 100.00% 21.41% 23.30% 13.19%
EUR 57.40% 48.04% 21.41% 100.00% 65.07% 32.49%
GBP 55.39% 46.85% 23.30% 65.07% 100.00% 19.22%
JPY 11.79% 5.78% 13.19% 32.49% 19.22% 100.00%

Analyze Exchange Rate Paths

Below are the simulated GBM paths for each currency exchange rate. Included are the max historical yearly changes to serve as a comparison for actual changes.

You can see that the majority of the simulated paths fit within 25-year historical max and mins, except where historical max and mins are assymetric.